Skip to content

Conversation

@ianmacartney
Copy link
Member

@ianmacartney ianmacartney commented Dec 5, 2025

Allow batching pending writes to reduce the amount of calls to the component during mutations that may be using triggers


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Summary by CodeRabbit

  • New Features

    • Batched write support: buffered, namespace-aware mixed inserts/updates/deletes with manual flush, auto-flush-before-read, trigger-friendly behavior, and performance comparison utilities.
    • New multi-operation batch API for processing ordered operations in a single call.
  • Documentation

    • Expanded README with “Optimizing Triggers with Batching”, batch write/read guidance, and a complete buffering example; added a detailed example demonstrating patterns.
  • Tests

    • Added tests covering buffering, manual flush, and auto-flush-in-mutation behavior.
  • Chores

    • Registered new example, bumped package version, and added changelog entry.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

📝 Walkthrough

Walkthrough

Adds client-side write buffering APIs to Aggregate (startBuffering/finishBuffering/flush) that queue write ops and flush them as a single batch; introduces a server-side batch mutation that processes mixed operations by namespace and reuses trees; allows injecting trees into btree handlers; adds example, tests, docs, and config registration.

Changes

Cohort / File(s) Summary
Client buffering implementation
src/client/index.ts
Adds buffering state (operationQueue, isBuffering, currentFlushPromise); new public APIs startBuffering(), finishBuffering(ctx), flush(ctx, opts?); enqueues write ops when buffering and flushes them as a single batch; ensures auto-flush before reads.
Batch mutation & per-op handlers
src/component/public.ts
Adds batch mutation that accepts mixed operations and processes them in order, factoring replace, deleteIfExists, replaceOrInsert into dedicated handler functions and caching per-namespace trees for reuse.
B-tree handler injection
src/component/btree.ts
Updates insertHandler and deleteHandler signatures to accept optional treeArg?: Doc<"btree"> so callers can inject/reuse a tree instance.
Examples & config
example/convex/batchedWrites.ts, example/convex/convex.config.ts
Adds example/convex/batchedWrites.ts with multiple exported batched-write mutations and patterns; registers the new aggregate with app.use(aggregate, { name: "batchedWrites" }) in convex.config.ts.
Tests
src/client/buffer.test.ts
Adds tests for DirectAggregate buffering: auto-flush in a mutation, manual flush, and read-consistency while buffering.
Docs, changelog & metadata
README.md, CHANGELOG.md, package.json
Adds “Optimizing Triggers with Batching” and “Batch Write Operations” sections and examples; adds changelog entry for buffering; bumps package version to 0.2.2-alpha.0.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Aggregate
  participant Buffer as BufferQueue
  participant Server as BatchMutation
  participant BTree as BTreeHandlers

  Client->>Aggregate: startBuffering()
  Aggregate-->>Client: buffering enabled

  Client->>Aggregate: insert(ctx, op1)
  Aggregate->>Buffer: enqueue op1

  Client->>Aggregate: insert(ctx, op2)
  Aggregate->>Buffer: enqueue op2

  Client->>Aggregate: finishBuffering(ctx)
  Aggregate->>Server: submitBatch([op1, op2])

  Server->>Server: group ops by namespace
  Server->>BTree: getOrCreate tree for namespace A
  Server->>BTree: insertHandler(op1, treeArg)
  BTree-->>Server: ok
  Server->>BTree: insertHandler(op2, treeArg)
  BTree-->>Server: ok

  Server-->>Aggregate: batch applied
  Aggregate->>Buffer: clear queue
  Aggregate-->>Client: finish complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I buffered carrots in a cozy heap,

Queued tiny hops for a single leap,
Trees shared roots and namespaces aligned,
Batches flushed — fewer trips to mind,
Now I hop once and munch in peace.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "batch writes" directly corresponds to the main feature introduced in this PR—batching pending write operations to reduce component calls during mutations using triggers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 5, 2025

Open in StackBlitz

npm i https://pkg.pr.new/get-convex/aggregate/@convex-dev/aggregate@167

commit: 462c727

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/client/buffer.test.ts (1)

20-53: Consider test isolation for the aggregate instance.

The aggregate instance is shared across both t.run blocks. If the first test block throws before reaching aggregate.buffer(false), the buffering state remains enabled for subsequent tests. Consider creating a fresh instance per test block or wrapping in try/finally to ensure cleanup.

src/client/index.ts (2)

39-81: Consider stronger typing for BufferedOperation.

Using any for keys, values, and namespaces loses type safety. While this may be intentional to avoid complex generics, it could allow type mismatches that would only be caught at runtime.


101-124: Buffering state is instance-level - document thread-safety expectations.

The isBuffering and operationQueue are instance fields. If the same Aggregate instance is used across concurrent mutations (e.g., multiple requests sharing a singleton), the buffering state could interfere. Consider documenting that buffered aggregates should not be shared across concurrent mutation contexts, or make the state context-scoped.

example/convex/batchedWrites.ts (2)

23-50: Missing return validators on all mutations.

Per coding guidelines, all Convex functions must include return validators. Each mutation in this file returns a value but lacks a returns: property.

Example fix for basicBatchedWrites:

 export const basicBatchedWrites = mutation({
   args: {
     count: v.number(),
   },
+  returns: v.object({
+    inserted: v.number(),
+    total: v.number(),
+  }),
   handler: async (ctx, { count }) => {

Apply similar changes to:

  • batchedWritesWithOnSuccess: returns: v.object({ queued: v.number() })
  • complexBatchedOperations: returns: v.object({ operations: v.object({ inserts: v.number(), deletes: v.number(), updates: v.number() }) })
  • comparePerformance: returns: v.object({ method: v.string(), count: v.number(), durationMs: v.number() })
  • autoFlushOnRead: returns: v.object({ queued: v.number(), totalInRange: v.number() })
  • batchedWritesWithNamespaces: returns: v.object({ operations: v.number(), namespaces: v.number(), message: v.string() })

Also applies to: 82-107, 112-180, 185-226, 231-264, 273-321


40-43: Inconsistent buffer/flush ordering.

Here buffering is disabled before flushing (lines 41, 43), but in complexBatchedOperations (lines 167, 170) the order is reversed (flush first, then disable). For clarity and consistency, consider adopting a uniform pattern—typically flush while still buffered, then disable.

-    // Disable buffering after we're done
-    aggregate.buffer(false);
     // Flush all buffered operations in a single batch
     await aggregate.flush(ctx);
+    // Disable buffering after flush
+    aggregate.buffer(false);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75b3a2f and 538ed4a.

⛔ Files ignored due to path filters (2)
  • example/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • src/component/_generated/component.ts is excluded by !**/_generated/**
📒 Files selected for processing (6)
  • example/convex/batchedWrites.ts (1 hunks)
  • example/convex/convex.config.ts (1 hunks)
  • src/client/buffer.test.ts (1 hunks)
  • src/client/index.ts (17 hunks)
  • src/component/btree.ts (2 hunks)
  • src/component/public.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/convex.config.ts
  • example/convex/batchedWrites.ts
🧠 Learnings (17)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • src/component/public.ts
  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • example/convex/convex.config.ts
  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • example/convex/convex.config.ts
  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules

Applied to files:

  • example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : By default Convex returns documents in ascending `_creationTime` order; use `.order('asc')` or `.order('desc')` to specify order

Applied to files:

  • example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • example/convex/convex.config.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • example/convex/convex.config.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/crons.{ts,tsx} : Define crons by declaring the top-level `crons` object, calling methods on it, and exporting it as default from `convex/crons.ts` or similar

Applied to files:

  • example/convex/convex.config.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/convex.config.ts
  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex

Applied to files:

  • example/convex/convex.config.ts
  • src/client/buffer.test.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • src/client/buffer.test.ts
  • src/client/index.ts
  • src/component/btree.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • src/client/buffer.test.ts
  • src/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`

Applied to files:

  • src/client/buffer.test.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • src/client/buffer.test.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • src/client/index.ts
🧬 Code graph analysis (6)
src/component/public.ts (1)
src/component/btree.ts (4)
  • getOrCreateTree (937-973)
  • DEFAULT_MAX_NODE_SIZE (25-25)
  • insertHandler (45-81)
  • deleteHandler (83-113)
example/convex/convex.config.ts (1)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/buffer.test.ts (3)
src/client/setup.test.ts (2)
  • componentSchema (6-6)
  • componentModules (7-7)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (2)
  • DirectAggregate (749-867)
  • count (160-173)
src/client/index.ts (1)
src/client/positions.ts (1)
  • keyToPosition (67-72)
src/component/btree.ts (1)
example/convex/_generated/dataModel.d.ts (1)
  • Doc (30-33)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (2)
  • DirectAggregate (749-867)
  • count (160-173)
🪛 GitHub Actions: Test and lint
src/client/buffer.test.ts

[error] 26-26: TypeScript error TS2339: Property 'aggregateDirect' does not exist on type '{ aggregate: ComponentApi; }'.

🪛 GitHub Check: Test and lint
src/client/buffer.test.ts

[failure] 26-26:
Property 'aggregateDirect' does not exist on type '{ aggregate: ComponentApi; }'.

🔇 Additional comments (9)
example/convex/convex.config.ts (1)

12-12: LGTM!

The new aggregate registration for batchedWrites follows the established pattern and is correctly placed with other aggregate registrations.

src/client/index.ts (2)

131-140: Operations are discarded if flush fails.

The queue is cleared before the mutation is awaited. If ctx.runMutation throws, the buffered operations are lost and cannot be retried. This may be intentional (prevents double-processing), but the caller has no way to recover.

Consider either:

  1. Documenting this behavior in the method's docstring
  2. Restoring the queue on failure:
   async flush(ctx: RunMutationCtx): Promise<void> {
     if (this.operationQueue.length === 0) {
       return;
     }
     const operations = this.operationQueue;
     this.operationQueue = [];
-    await ctx.runMutation(this.component.public.batch, {
-      operations,
-    });
+    try {
+      await ctx.runMutation(this.component.public.batch, {
+        operations,
+      });
+    } catch (e) {
+      // Restore operations on failure so caller can retry
+      this.operationQueue = [...operations, ...this.operationQueue];
+      throw e;
+    }
   }

164-164: Good coverage of flush-before-read across all query paths.

All read operations (count, countBatch, sum, sumBatch, at, atBatch, indexOf, paginate, paginateNamespaces) correctly call flushBeforeRead. Derived methods like min, max, random, and iter also benefit through their use of these primitives.

src/component/btree.ts (2)

45-57: LGTM!

The optional treeArg parameter cleanly enables tree reuse for batch operations while preserving existing behavior when not provided.


83-95: LGTM!

Consistent pattern with insertHandler for the optional tree injection.

src/component/public.ts (2)

229-240: Operation order not preserved across namespaces.

Grouping by namespace means operations are reordered. For example, [insert ns:A, insert ns:B, delete ns:A] becomes [insert A, delete A] then [insert B]. If this reordering is intentional for performance, consider documenting it. If original order must be preserved, process operations in input order while caching trees.

If order must be preserved:

   handler: async (ctx, { operations }) => {
-    // Group operations by namespace to fetch each tree once
-    const namespaceGroups = new Map<string, typeof operations>();
-    for (const op of operations) {
-      ...
-    }
-
-    // Process each namespace group
-    for (const [namespaceKey, ops] of namespaceGroups.entries()) {
-      ...
-    }
+    // Cache trees by namespace to avoid repeated fetches
+    const treeCache = new Map<string, Doc<"btree">>();
+    
+    for (const op of operations) {
+      const namespace = "namespace" in op ? op.namespace : undefined;
+      const key = namespace === undefined ? "__undefined__" : JSON.stringify(namespace);
+      
+      if (!treeCache.has(key)) {
+        treeCache.set(key, await getOrCreateTree(ctx.db, namespace, DEFAULT_MAX_NODE_SIZE, true));
+      }
+      const tree = treeCache.get(key)!;
+      
+      // Process operation with cached tree...
+    }
   },

336-363: insertIfDoesNotExist replaces existing items rather than no-op.

The comment notes it's "implemented as replaceOrInsert", which means if the key exists, it gets deleted and re-inserted. This is consistent with the non-batch _insertIfDoesNotExist implementation, but the name suggests it should be a no-op if the item exists.

Verify this semantic is intentional. If true "insert if not exists" is needed:

         } else if (op.type === "insertIfDoesNotExist") {
-          // insertIfDoesNotExist is implemented as replaceOrInsert
-          try {
-            await deleteHandler(...);
-          } catch (e) { ... }
-          await insertHandler(...);
+          // True insert-if-not-exists: try insert, ignore duplicate key error
+          try {
+            await insertHandler(
+              ctx,
+              {
+                key: op.key,
+                value: op.value,
+                summand: op.summand,
+                namespace: op.namespace,
+              },
+              tree,
+            );
+          } catch (e) {
+            if (!(e instanceof ConvexError && e.data?.code === "DUPLICATE_KEY")) {
+              throw e;
+            }
+          }
example/convex/batchedWrites.ts (2)

286-290: Good pattern: local aggregate instance.

Creating the DirectAggregate inside the handler avoids shared mutable state issues. This is the recommended approach compared to the module-level instance used elsewhere in this file.


15-18: Move DirectAggregate instantiation into each mutation handler.

Module-level mutable state violates Convex's execution model. The aggregate instance maintains state via this.isBuffering and this.operationQueue, which are not reliably preserved across function invocations. Convex executes mutations in isolated contexts and may re-run or move functions between runtimes, causing module-level state to be lost and breaking transactional guarantees. Instantiate DirectAggregate inside each handler instead, as done in batchedWritesWithNamespaces (lines 286–290), so each invocation has its own isolated state.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @example/convex/batchedWrites.ts:
- Around line 38-63: The mutation basicBatchedWrites is missing a returns
validator and also fails to await the async aggregate.finishBuffering call; add
a returns validator to the mutation (matching the returned object shape, e.g.,
inserted and total as numbers) via the mutation({ args: ..., returns: ... })
signature and change the finishBuffering call to await
aggregate.finishBuffering(ctx) so the buffering completes before reading
aggregate.count(ctx).

In @src/client/index.ts:
- Around line 148-159: The error thrown in flushBeforeRead references a
non-existent .buffer(false) method; update the message to reference the real
API: mention finishBuffering() or flush() instead. In the flushBeforeRead method
(check isBuffering, operationQueue, and the "runMutation" in ctx branch),
replace the string that suggests ".buffer(false)" with a corrected instruction
such as calling finishBuffering() or flush() before reading so the error message
matches available methods.
- Around line 106-121: The docstring for startBuffering() incorrectly references
stopBuffering(); update the documentation example to call finishBuffering()
instead (e.g., replace "await aggregate.stopBuffering(ctx)" with "await
aggregate.finishBuffering(ctx)") and ensure the docstring text and example
consistently use the actual method name finishBuffering() so consumers see the
correct API.
🧹 Nitpick comments (7)
example/convex/batchedWrites.ts (7)

114-135: Missing return validator.

Per coding guidelines, all Convex functions should include return validators.

📝 Suggested fix
 export const addMultipleScores = mutationWithTriggers({
   args: {
     scores: v.array(
       v.object({
         name: v.string(),
         score: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    inserted: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { scores }) => {

165-208: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const compareTriggersWithAndWithoutBatching = mutation({
   args: {
     count: v.number(),
     useBatching: v.boolean(),
   },
+  returns: v.object({
+    method: v.string(),
+    count: v.number(),
+    durationMs: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { count, useBatching }) => {

213-278: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const complexBatchedOperations = mutation({
   args: {
     inserts: v.array(
       v.object({
         key: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
     deletes: v.array(
       v.object({
         key: v.number(),
         id: v.string(),
       }),
     ),
     updates: v.array(
       v.object({
         oldKey: v.number(),
         newKey: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    operations: v.object({
+      inserts: v.number(),
+      deletes: v.number(),
+      updates: v.number(),
+    }),
+  }),
   handler: async (ctx, { inserts, deletes, updates }) => {

283-323: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const comparePerformance = mutation({
   args: {
     count: v.number(),
     useBatching: v.boolean(),
   },
+  returns: v.object({
+    method: v.string(),
+    count: v.number(),
+    durationMs: v.number(),
+  }),
   handler: async (ctx, { count, useBatching }) => {

328-361: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const autoFlushOnRead = mutation({
   args: {
     count: v.number(),
   },
+  returns: v.object({
+    queued: v.number(),
+    totalInRange: v.number(),
+  }),
   handler: async (ctx, { count }) => {

370-415: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const batchedWritesWithNamespaces = mutation({
   args: {
     operations: v.array(
       v.object({
         namespace: v.string(),
         key: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    operations: v.number(),
+    namespaces: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { operations }) => {

140-160: Missing return validator.

Per coding guidelines, this mutation should include a returns validator.

📝 Suggested fix
 export const updateMultipleScores = mutationWithTriggers({
   args: {
     updates: v.array(
       v.object({
         id: v.id("leaderboard"),
         newScore: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    updated: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { updates }) => {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c05f07 and a40535e.

📒 Files selected for processing (3)
  • README.md
  • example/convex/batchedWrites.ts
  • src/client/index.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/batchedWrites.ts
🧠 Learnings (14)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • src/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • src/client/index.ts
  • README.md
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.

Applied to files:

  • example/convex/batchedWrites.ts
🧬 Code graph analysis (2)
src/client/index.ts (2)
src/component/_generated/component.ts (1)
  • ComponentApi (24-288)
src/client/positions.ts (1)
  • keyToPosition (67-72)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (3)
  • DirectAggregate (755-873)
  • TableAggregate (908-1059)
  • count (166-179)
🔇 Additional comments (7)
README.md (2)

492-559: Documentation looks good for the new batching feature.

The documentation clearly explains the benefits of batching with triggers and provides a complete, working example. The pattern shown aligns with the implementation in src/client/index.ts.


618-656: Well-documented batch write operations section.

The documentation clearly explains the batching API, its benefits, and provides a simple usage example. The cross-references to other sections and the example file are helpful.

src/client/index.ts (4)

39-81: BufferedOperation type definition looks correct.

The discriminated union covers all mutation types (insert, delete, replace, deleteIfExists, replaceOrInsert, insertIfDoesNotExist) and matches the batch operation schema in src/component/_generated/component.ts lines 157-193.


137-146: Consider error handling behavior in flush.

If ctx.runMutation fails after the queue is cleared (line 142), the buffered operations are lost. This may be intentional (mutations in Convex are retried by the runtime), but if the mutation is abandoned after retries, the operations won't be persisted.

Since Convex handles retries at the mutation level and the entire mutation would be retried from the beginning (re-populating the buffer), this behavior is likely correct. However, it's worth confirming this is the intended semantics.


486-502: Write operation buffering implementation looks correct.

The pattern of checking isBuffering and either queueing or executing directly is consistent across all write methods. The queued operations use the same keyToPosition transformation as the direct mutation calls.


166-179: Read operations correctly flush before executing.

All read methods (count, countBatch, sum, sumBatch, at, atBatch, indexOf, paginate, paginateNamespaces) properly await flushBeforeRead(ctx) before executing, ensuring read-after-write consistency when buffering is enabled.

example/convex/batchedWrites.ts (1)

88-106: Custom mutation pattern looks correct.

This demonstrates the recommended pattern for combining triggers with buffering. The buffering is enabled before the mutation runs and flushed in the onSuccess callback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In @example/convex/batchedWrites.ts:
- Around line 35-40: The handler is creating a floating promise by calling
finishBuffering without awaiting it, so the subsequent count() (and other reads)
can observe unflushed writes; update the handlers in
example/convex/batchedWrites.ts (including the reset mutation and the other
mutation(s) around lines 45–77) to await finishBuffering before proceeding or
returning (e.g., await finishBuffering(...)) so flushing completes before
calling count() or finishing the mutation.
- Around line 1-10: Update the example header to reference the new public API
names instead of the old `.buffer()` call: change the text to mention
startBuffering, finishBuffering and flush as the methods to queue and flush
batched write operations (e.g., "use startBuffering/finishBuffering/flush to
queue and flush writes"), and ensure any explanatory sentences that describe
`.buffer()` behavior are updated to describe the new
startBuffering/finishBuffering/flush workflow used in this file
(batchedWrites.ts).
- Around line 35-439: Several exported mutations in this file are missing the
required returns validators; update each exported call to include a returns:
validator matching the actual return shape (or v.null() if nothing is returned).
For example, add returns: v.null() to reset and other void-returning mutations,
add returns: v.object({ inserted: v.number(), total: v.number() }) to
basicBatchedWrites, add appropriate object validators to addMultipleScores,
updateMultipleScores, complexBatchedOperations, comparePerformance,
autoFlushOnRead, batchedWritesWithNamespaces, and
compareTriggersWithAndWithoutBatching, and ensure
mutationWithTriggers/customMutation declarations also include returns when they
export a handler that returns a value; locate these by the exported symbols
reset, basicBatchedWrites, mutationWithTriggers, addMultipleScores,
updateMultipleScores, compareTriggersWithAndWithoutBatching,
complexBatchedOperations, comparePerformance, autoFlushOnRead, and
batchedWritesWithNamespaces and add the matching returns validators.

In @src/client/index.ts:
- Around line 120-123: finishBuffering currently sets isBuffering = false before
awaiting flush(), which allows concurrent reads to bypass flushBeforeRead while
writes are still pending; change the order so you await this.flush(ctx) first
and only set this.isBuffering = false after the flush completes successfully (do
not swallow flush errors so buffering remains enabled on failure); reference:
finishBuffering, isBuffering, flush, and flushBeforeRead.
- Around line 141-152: The error string in flushBeforeRead references a
non-existent .buffer(false); update the message in flushBeforeRead to accurately
instruct users: when this.isBuffering && this.operationQueue.length > 0 and the
ctx is a query (no runMutation), throw an Error that tells callers they cannot
read with buffered operations and should either call flush(ctx) before reading,
finishBuffering() to stop buffering, or perform the action from a mutation
context; keep references to flushBeforeRead, isBuffering, operationQueue, flush,
finishBuffering, runMutation, and the RunQueryCtx/RunMutationCtx types so
reviewers can locate and verify the change.
- Around line 120-139: flush() currently clears this.operationQueue before
calling ctx.runMutation, losing ops if runMutation throws; change flush to
preserve the queue on failure by capturing the ops to send (e.g., const
operations = this.operationQueue.slice()) and only mutate this.operationQueue
(clear or remove those sent) after runMutation succeeds; use try/catch to
rethrow errors while leaving this.operationQueue intact on failure and reference
the methods/fields: flush, finishBuffering, this.operationQueue,
ctx.runMutation, and this.component.public.batch to locate where to implement
the change.
- Around line 99-123: The docstrings reference old API names (`stopBuffering`,
`.buffer(false)`) that no longer exist after the rename to
startBuffering/finishBuffering; update the example and any mentions in the
comments to call finishBuffering (and/or await aggregate.finishBuffering(ctx))
and remove references to `.buffer(false)`, and ensure the narrative mentions
that finishBuffering will call flush(ctx) to send queued operations (refer to
startBuffering, finishBuffering, flush, and RunMutationCtx to locate the code).

In @src/component/public.ts:
- Around line 315-331: The replace branch is not forwarding op.namespace and
op.newNamespace into the args passed to replaceHandler, so replaceHandler (and
downstream deleteHandler/insertHandler) lose namespace context; update the call
to replaceHandler in the block handling op.type === "replace" to include
namespace and newNamespace fields (e.g., add namespace: op.namespace and
newNamespace: op.newNamespace to the second argument), while still resolving
deleteTree/insertTree via
getTreeForNamespace(op.namespace)/getTreeForNamespace(op.newNamespace) so that
replaceHandler and its deleteHandler/insertHandler children receive the correct
namespace values for behavior, logging, and invariants.
- Around line 230-289: getTreeForNamespace currently uses
JSON.stringify(namespace) which throws on BigInt; update it to serialize
namespaces via convexToJson before stringifying so keys handle BigInt and other
Convex types. Replace JSON.stringify(namespace) with
JSON.stringify(convexToJson(namespace)) when computing the sentinel key and keep
the existing sentinel for undefined; ensure you still call
getOrCreateTree(ctx.db, namespace, DEFAULT_MAX_NODE_SIZE, true) and store its
ReturnType in treesMap keyed by the new serialized key.
🧹 Nitpick comments (1)
src/client/index.ts (1)

39-75: Avoid any for buffered ops (at least use unknown / shared Key types).
BufferedOperation being any-heavy weakens the main value of batching (catching shape mismatches at compile time). Consider typing key as Key, namespace as ConvexValue | undefined, and value as ID (or unknown) where possible.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a40535e and 0d25f49.

⛔ Files ignored due to path filters (2)
  • example/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • src/component/_generated/component.ts is excluded by !**/_generated/**
📒 Files selected for processing (7)
  • README.md
  • example/convex/batchedWrites.ts
  • example/convex/convex.config.ts
  • src/client/buffer.test.ts
  • src/client/index.ts
  • src/component/btree.ts
  • src/component/public.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/buffer.test.ts
  • example/convex/convex.config.ts
  • src/component/btree.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/batchedWrites.ts
🧠 Learnings (18)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • README.md
  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • src/component/public.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • src/component/public.ts
  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • src/component/public.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist

Applied to files:

  • src/component/public.ts
  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • src/component/public.ts
  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the database

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex storage stores items as `Blob` objects; convert all items to/from a `Blob` when using Convex storage

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • src/component/public.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • src/component/public.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • src/component/public.ts
  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.

Applied to files:

  • example/convex/batchedWrites.ts
🧬 Code graph analysis (2)
src/component/public.ts (1)
src/component/btree.ts (7)
  • Key (27-27)
  • Value (28-28)
  • Namespace (29-29)
  • deleteHandler (83-113)
  • insertHandler (45-81)
  • getOrCreateTree (949-985)
  • DEFAULT_MAX_NODE_SIZE (25-25)
src/client/index.ts (2)
src/component/_generated/component.ts (1)
  • ComponentApi (24-281)
src/client/positions.ts (1)
  • keyToPosition (70-75)
🪛 GitHub Actions: Test and lint
example/convex/batchedWrites.ts

[error] 65-65: ESLint: no-floating-promises violation. Promises must be awaited, end with a call to .catch or .then with a rejection handler, or be explicitly marked as ignored with the void operator.

🪛 GitHub Check: Test and lint
example/convex/batchedWrites.ts

[failure] 65-65:
Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator

🔇 Additional comments (8)
src/client/index.ts (2)

159-324: Auto-flush-before-read integration looks consistent.
Good coverage: count/sum/at/indexOf/paginate/paginateNamespaces all gate reads through flushBeforeRead, which matches the buffering contract.

Also applies to: 398-429, 654-673


471-610: Buffered write enqueuing is straightforward and preserves operation order.
The enqueue shapes match the component batch API (insert/delete/replace/deleteIfExists/replaceOrInsert).

src/component/public.ts (3)

74-118: Handler extraction + optional tree injection is a clean batch-enabler.
replaceHandler delegating to deleteHandler + insertHandler with injectable trees is a good reuse point for the new batch API.


226-358: Batch op loop preserves caller order; reuse of per-namespace trees is clear.
The sequential processing matches the expectation of deterministic “same order as provided” semantics while still removing repeated tree fetches.


125-138: The error code "DELETE_MISSING_KEY" is the canonical code used in this codebase. It's defined in src/component/btree.ts:541 and thrown by deleteFromNode when a key is not found. The error handling in the deleteIfExistsHandler function correctly catches this error and suppresses it as intended.

example/convex/batchedWrites.ts (2)

94-233: The example coverage is nice (direct batching, trigger batching, namespaces).
Once the await + returns issues are fixed, this should be a solid reference for users.

Also applies to: 234-347, 349-439


102-121: Use customCtx helper to preserve full context when wrapping with triggers.

The pattern at lines 102-120 should import and use customCtx(triggers.wrapDB) instead of manually reconstructing the context with only ...triggers.wrapDB(ctx). Follow the established pattern in leaderboard.ts (line 53) and photos.ts (line 40):

const mutationWithTriggers = customMutation(
  mutation,
  customCtx(triggers.wrapDB),
);

Import customCtx from "convex-helpers/server/customFunctions" and apply it to the wrapper function. The current manual reconstruction loses context properties beyond the wrapped db, which can cause issues if the handler or aggregate methods need access to other context functionality.

Also applies to: 128-185

⛔ Skipped due to learnings
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the database
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runAction` to call an action from another action; otherwise pull out shared code into a helper async function
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Do NOT use deprecated `ctx.storage.getMetadata`; instead query the `_storage` system table using `ctx.db.system.get`
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property
README.md (1)

578-657: Batch read/write docs align with the new API naming.
The renamed “Batch Read Operations” and the new “Batch Write Operations” section match countBatch/sumBatch/atBatch and startBuffering/finishBuffering usage.

Comment on lines 35 to 439
export const reset = internalMutation({
args: {},
handler: async (ctx) => {
await aggregate.clearAll(ctx);
},
});

/**
* Basic example: Enable buffering, queue operations, then flush manually.
*/
export const basicBatchedWrites = internalMutation({
args: {
count: v.number(),
},
handler: async (ctx, { count }) => {
// Enable buffering mode - modifies the aggregate instance in place
aggregate.startBuffering();

const initialCount = await aggregate.count(ctx);

// Queue multiple insert operations
for (let i = 0; i < count; i++) {
await aggregate.insert(ctx, {
key: i + initialCount,
id: `item-${i}`,
sumValue: i * 10,
});
}

// Disable buffering after we're done
aggregate.finishBuffering(ctx);

// Read operations work normally (and auto-flush if needed)
const total = await aggregate.count(ctx);

if (total !== initialCount + count) {
console.log({ initialCount, count, total });
throw new Error("Total count is incorrect");
}

return { inserted: count, total };
},
});

/**
* Advanced example: Use custom functions with Triggers and buffering.
*
* This is the RECOMMENDED pattern when using triggers!
*
* When using triggers, each table write triggers an aggregate write.
* If you insert 100 rows, that's 100 separate calls to the aggregate component.
* With buffering, all 100 writes are batched into a single component call.
*
* Performance benefits:
* - Single component call instead of N calls
* - Single tree fetch instead of N fetches
* - Better handling of write contention
*/

// Set up triggers
const triggers = new Triggers<DataModel>();
triggers.register("leaderboard", leaderboardAggregate.trigger());

// Create a custom mutation that:
// 1. Wraps the database with triggers
// 2. Enables buffering before the mutation runs
// 3. Flushes after the mutation completes successfully
const mutationWithTriggers = customMutation(mutation, {
args: {},
input: async (ctx) => {
// Enable buffering for all aggregate operations
leaderboardAggregate.startBuffering();

return {
ctx: {
// Wrap db with triggers
...triggers.wrapDB(ctx),
},
args: {},
onSuccess: async ({ ctx }) => {
// Flush all buffered operations in a single batch
await leaderboardAggregate.finishBuffering(ctx);
},
};
},
});

/**
* Example: Add multiple scores with triggers and batching.
*
* Without buffering: Each insert triggers a separate aggregate.insert call
* With buffering: All inserts are batched into one aggregate.batch call
*/
export const addMultipleScores = mutationWithTriggers({
args: {
scores: v.array(
v.object({
name: v.string(),
score: v.number(),
}),
),
},
handler: async (ctx, { scores }) => {
const initialSumValue = await leaderboardAggregate.sum(ctx);

// Just insert into the table - the trigger automatically
// updates the aggregate, and buffering batches all the updates
for (const { name, score } of scores) {
await ctx.db.insert("leaderboard", { name, score });
}

const totalSumValue = await leaderboardAggregate.sum(ctx);

if (
totalSumValue !==
initialSumValue + scores.reduce((acc, { score }) => acc + score, 0)
) {
throw new Error("Total sum value is incorrect");
}

return {
inserted: scores.length,
message: `Added ${scores.length} scores with batched aggregate updates`,
};
},
});

/**
* Example: Update multiple scores - shows replace operations are also batched
*/
export const updateMultipleScores = mutationWithTriggers({
args: {
updates: v.array(
v.object({
id: v.id("leaderboard"),
newScore: v.number(),
}),
),
},
handler: async (ctx, { updates }) => {
// Each patch triggers aggregate.replace, all batched together
for (const { id, newScore } of updates) {
await ctx.db.patch(id, { score: newScore });
}

return {
updated: updates.length,
message: `Updated ${updates.length} scores with batched aggregate updates`,
};
},
});

/**
* Example showing the difference with and without batching
*/
export const compareTriggersWithAndWithoutBatching = mutation({
args: {
count: v.number(),
useBatching: v.boolean(),
},
handler: async (ctx, { count, useBatching }) => {
console.time();

const customCtx = triggers.wrapDB(ctx);
if (useBatching) {
// With batching: all aggregate operations batched into one call
leaderboardAggregate.startBuffering();

for (let i = 0; i < count; i++) {
await customCtx.db.insert("leaderboard", {
name: `player-${i}`,
score: Math.floor(Math.random() * 1000),
});
}

await leaderboardAggregate.finishBuffering(ctx);
} else {
// Without batching: each insert makes a separate aggregate call

for (let i = 0; i < count; i++) {
await customCtx.db.insert("leaderboard", {
name: `player-${i}`,
score: Math.floor(Math.random() * 1000),
});
}
}

console.timeEnd();

return {
method: useBatching ? "with batching" : "without batching",
count,
message: useBatching
? `1 batched call to aggregate component`
: `${count} individual calls to aggregate component`,
};
},
});

/**
* Complex example: Mix different operation types in a batch.
*/
export const complexBatchedOperations = mutation({
args: {
inserts: v.array(
v.object({
key: v.number(),
id: v.string(),
value: v.number(),
}),
),
deletes: v.array(
v.object({
key: v.number(),
id: v.string(),
}),
),
updates: v.array(
v.object({
oldKey: v.number(),
newKey: v.number(),
id: v.string(),
value: v.number(),
}),
),
},
handler: async (ctx, { inserts, deletes, updates }) => {
// Enable buffering
aggregate.startBuffering();

// Queue inserts
for (const item of inserts) {
await aggregate.insert(ctx, {
key: item.key,
id: item.id,
sumValue: item.value,
});
}

// Queue deletes
for (const item of deletes) {
await aggregate.deleteIfExists(ctx, {
key: item.key,
id: item.id,
});
}

// Queue updates (replace operations)
for (const item of updates) {
await aggregate.replaceOrInsert(
ctx,
{ key: item.oldKey, id: item.id },
{ key: item.newKey, sumValue: item.value },
);
}

// Flush all operations at once and stop buffering
await aggregate.finishBuffering(ctx);

return {
operations: {
inserts: inserts.length,
deletes: deletes.length,
updates: updates.length,
},
};
},
});

/**
* Performance comparison: Batched vs unbatched writes.
*/
export const comparePerformance = mutation({
args: {
count: v.number(),
useBatching: v.boolean(),
},
handler: async (ctx, { count, useBatching }) => {
const start = Date.now();

if (useBatching) {
// Batched approach
aggregate.startBuffering();

for (let i = 0; i < count; i++) {
await aggregate.insert(ctx, {
key: 1000000 + i,
id: `perf-test-${i}`,
sumValue: i,
});
}

await aggregate.finishBuffering(ctx);
} else {
// Unbatched approach
for (let i = 0; i < count; i++) {
await aggregate.insert(ctx, {
key: 1000000 + i,
id: `perf-test-${i}`,
sumValue: i,
});
}
}

const duration = Date.now() - start;

return {
method: useBatching ? "batched" : "unbatched",
count,
durationMs: duration,
};
},
});

/**
* Example showing automatic flush on read operations.
*/
export const autoFlushOnRead = mutation({
args: {
count: v.number(),
},
handler: async (ctx, { count }) => {
// Enable buffering
aggregate.startBuffering();

// Queue some operations
for (let i = 0; i < count; i++) {
await aggregate.insert(ctx, {
key: 2000000 + i,
id: `auto-flush-${i}`,
sumValue: i,
});
}

// This read operation automatically flushes the buffer first
// So we'll see the correct count including the queued operations
const total = await aggregate.count(ctx, {
bounds: {
lower: { key: 2000000, inclusive: true },
},
});

// Flush all operations at once and stop buffering
await aggregate.finishBuffering(ctx);

return {
queued: count,
totalInRange: total,
};
},
});

/**
* Example: Batch operations with namespace grouping.
*
* When you have operations across multiple namespaces,
* the batch mutation automatically groups them and fetches
* each namespace's tree only once.
*/
export const batchedWritesWithNamespaces = mutation({
args: {
operations: v.array(
v.object({
namespace: v.string(),
key: v.number(),
id: v.string(),
value: v.number(),
}),
),
},
handler: async (ctx, { operations }) => {
// Create a namespaced aggregate
const namespacedAggregate = new DirectAggregate<{
Key: number;
Id: string;
Namespace: string;
}>(components.batchedWrites);

// Enable buffering
namespacedAggregate.startBuffering();

// Queue operations - they'll be grouped by namespace internally
for (const op of operations) {
await namespacedAggregate.insert(ctx, {
namespace: op.namespace,
key: op.key,
id: op.id,
sumValue: op.value,
});
}

// Flush all operations and stop buffering
// The batch mutation will group by namespace automatically
await namespacedAggregate.finishBuffering(ctx);

// Count unique namespaces
const namespaces = new Set(operations.map((op) => op.namespace));

return {
operations: operations.length,
namespaces: namespaces.size,
message: `Processed ${operations.length} operations across ${namespaces.size} namespaces in a single batch`,
};
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add returns validators to all exported functions in example/convex/**.
Per the repo’s example/convex guidelines, every mutation / internalMutation must declare both args and returns (use returns: v.null() if nothing is returned). This file currently omits returns in many exports (e.g. reset, basicBatchedWrites, compareTriggersWithAndWithoutBatching, etc.).

Based on coding guidelines, add returns throughout (e.g. basicBatchedWrites could return v.object({ inserted: v.number(), total: v.number() }), others v.null() or appropriate objects).

🧰 Tools
🪛 GitHub Actions: Test and lint

[error] 65-65: ESLint: no-floating-promises violation. Promises must be awaited, end with a call to .catch or .then with a rejection handler, or be explicitly marked as ignored with the void operator.

🪛 GitHub Check: Test and lint

[failure] 65-65:
Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator

🤖 Prompt for AI Agents
In @example/convex/batchedWrites.ts around lines 35 - 439, Several exported
mutations in this file are missing the required returns validators; update each
exported call to include a returns: validator matching the actual return shape
(or v.null() if nothing is returned). For example, add returns: v.null() to
reset and other void-returning mutations, add returns: v.object({ inserted:
v.number(), total: v.number() }) to basicBatchedWrites, add appropriate object
validators to addMultipleScores, updateMultipleScores, complexBatchedOperations,
comparePerformance, autoFlushOnRead, batchedWritesWithNamespaces, and
compareTriggersWithAndWithoutBatching, and ensure
mutationWithTriggers/customMutation declarations also include returns when they
export a handler that returns a value; locate these by the exported symbols
reset, basicBatchedWrites, mutationWithTriggers, addMultipleScores,
updateMultipleScores, compareTriggersWithAndWithoutBatching,
complexBatchedOperations, comparePerformance, autoFlushOnRead, and
batchedWritesWithNamespaces and add the matching returns validators.

Comment on lines +492 to +559
### Optimizing Triggers with Batching

**Recommended:** When using triggers, combine them with the batching API for
optimal performance.

Without batching, each table write triggers a separate call to the aggregate
component. If you insert 100 rows in a mutation, that's 100 individual calls to
the aggregate component, each fetching and updating the B-tree separately.

With batching, all triggered aggregate operations are queued and sent as a
single batch at the end of the mutation. This provides:

- **Single component call** instead of N separate calls
- **Single tree fetch** instead of N fetches
- **Better write contention handling** - one atomic update instead of many
- **Significant performance improvement** - especially for bulk operations

Here's how to set it up:

```ts
import { TableAggregate } from "@convex-dev/aggregate";
import { Triggers } from "convex-helpers/server/triggers";
import { customMutation } from "convex-helpers/server/customFunctions";
import { mutation as rawMutation } from "./_generated/server";

const aggregate = new TableAggregate<{
Key: number;
DataModel: DataModel;
TableName: "leaderboard";
}>(components.aggregate, {
sortKey: (doc) => -doc.score,
});

// Set up triggers
const triggers = new Triggers<DataModel>();
triggers.register("leaderboard", aggregate.trigger());

// Create a custom mutation that enables buffering and flushes on success
const mutation = customMutation(rawMutation, {
args: {},
input: async (ctx) => {
aggregate.startBuffering();
return {
ctx: triggers.wrapDB(ctx),
args: {},
onSuccess: async ({ ctx }) => {
await aggregate.finishBuffering(ctx);
},
};
},
});

// Now use this mutation in your functions
export const addScores = mutation({
args: { scores: v.array(v.object({ name: v.string(), score: v.number() })) },
handler: async (ctx, { scores }) => {
// Each insert triggers an aggregate operation, but they're all batched!
for (const { name, score } of scores) {
await ctx.db.insert("leaderboard", { name, score });
}
// The flush happens automatically in the onSuccess callback
},
});
```

See [`example/convex/batchedWrites.ts`](example/convex/batchedWrites.ts) for a
complete working example with performance comparisons.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Batching-with-triggers snippet is missing key imports/context (components, DataModel, v).
In the added block, the snippet references components.aggregate, DataModel, and uses v later, but doesn’t show those imports (or where they come from). Consider adding the missing imports or a short comment that it’s an excerpt.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (10)
example/convex/batchedWrites.ts (10)

35-40: Add return validator for consistency with coding guidelines.

The function should include returns: v.null() since it doesn't return a value. This aligns with the guideline to always include return validators for all Convex functions.

♻️ Add return validator
 export const reset = internalMutation({
   args: {},
+  returns: v.null(),
   handler: async (ctx) => {
     await aggregate.clearAll(ctx);
   },
 });

Based on coding guidelines.


45-77: Add return validator for type safety.

The function returns an object with inserted and total properties but lacks a return validator. Adding one improves type safety and aligns with coding guidelines.

♻️ Add return validator
 export const basicBatchedWrites = internalMutation({
   args: {
     count: v.number(),
   },
+  returns: v.object({
+    inserted: v.number(),
+    total: v.number(),
+  }),
   handler: async (ctx, { count }) => {

Based on coding guidelines.


128-160: Add return validator for consistency.

The function returns an object with inserted and message properties but lacks a return validator.

♻️ Add return validator
 export const addMultipleScores = mutationWithTriggers({
   args: {
     scores: v.array(
       v.object({
         name: v.string(),
         score: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    inserted: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { scores }) => {

Based on coding guidelines.


165-185: Add return validator for consistency.

The function returns an object with updated and message properties but lacks a return validator.

♻️ Add return validator
 export const updateMultipleScores = mutationWithTriggers({
   args: {
     updates: v.array(
       v.object({
         id: v.id("leaderboard"),
         newScore: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    updated: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { updates }) => {

Based on coding guidelines.


196-196: Add label to console.time for clarity.

Using console.time() without a label makes it unclear which timer is being measured, especially if multiple timers run concurrently. Add a descriptive label.

♻️ Add timer label
-    console.time();
+    console.time("compareTriggersWithAndWithoutBatching");

And correspondingly update the timeEnd call on line 222:

-    console.timeEnd();
+    console.timeEnd("compareTriggersWithAndWithoutBatching");

190-232: Add return validator for consistency.

The function returns an object with method, count, and message properties but lacks a return validator.

♻️ Add return validator
 export const compareTriggersWithAndWithoutBatching = mutation({
   args: {
     count: v.number(),
     useBatching: v.boolean(),
   },
+  returns: v.object({
+    method: v.string(),
+    count: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { count, useBatching }) => {

Based on coding guidelines.


237-302: Add return validator for consistency.

The function returns an object with a nested operations object but lacks a return validator.

♻️ Add return validator
 export const complexBatchedOperations = mutation({
   args: {
     inserts: v.array(
       v.object({
         key: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
     deletes: v.array(
       v.object({
         key: v.number(),
         id: v.string(),
       }),
     ),
     updates: v.array(
       v.object({
         oldKey: v.number(),
         newKey: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    operations: v.object({
+      inserts: v.number(),
+      deletes: v.number(),
+      updates: v.number(),
+    }),
+  }),
   handler: async (ctx, { inserts, deletes, updates }) => {

Based on coding guidelines.


307-347: Add return validator for consistency.

The function returns an object with method, count, and durationMs properties but lacks a return validator.

♻️ Add return validator
 export const comparePerformance = mutation({
   args: {
     count: v.number(),
     useBatching: v.boolean(),
   },
+  returns: v.object({
+    method: v.string(),
+    count: v.number(),
+    durationMs: v.number(),
+  }),
   handler: async (ctx, { count, useBatching }) => {

Based on coding guidelines.


352-385: Add return validator for consistency.

The function returns an object with queued and totalInRange properties but lacks a return validator.

♻️ Add return validator
 export const autoFlushOnRead = mutation({
   args: {
     count: v.number(),
   },
+  returns: v.object({
+    queued: v.number(),
+    totalInRange: v.number(),
+  }),
   handler: async (ctx, { count }) => {

Based on coding guidelines.


394-439: Add return validator for consistency.

The function returns an object with operations, namespaces, and message properties but lacks a return validator.

♻️ Add return validator
 export const batchedWritesWithNamespaces = mutation({
   args: {
     operations: v.array(
       v.object({
         namespace: v.string(),
         key: v.number(),
         id: v.string(),
         value: v.number(),
       }),
     ),
   },
+  returns: v.object({
+    operations: v.number(),
+    namespaces: v.number(),
+    message: v.string(),
+  }),
   handler: async (ctx, { operations }) => {

Based on coding guidelines.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d25f49 and 3efa4bc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • CHANGELOG.md
  • example/convex/batchedWrites.ts
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/batchedWrites.ts
🧠 Learnings (17)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas

Applied to files:

  • example/convex/batchedWrites.ts
🧬 Code graph analysis (1)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (3)
  • DirectAggregate (738-856)
  • TableAggregate (891-1042)
  • count (159-172)
🔇 Additional comments (4)
CHANGELOG.md (1)

3-5: LGTM! Clear documentation of the buffering feature.

The changelog entry accurately describes the new buffering writes feature and its performance benefit when using triggers.

example/convex/batchedWrites.ts (3)

1-18: LGTM! Excellent documentation and proper imports.

The file header clearly explains the buffering pattern and its benefits, especially for trigger-based workflows. Imports are appropriate for the demonstrated functionality.


20-33: LGTM! Proper aggregate configuration.

Both DirectAggregate and TableAggregate instances are correctly configured. The use of negative score for descending order in the leaderboard is a proper pattern.


94-120: LGTM! Excellent pattern for trigger-based buffering.

The custom mutation wrapper correctly implements the buffering lifecycle: enabling before the mutation runs, wrapping the database with triggers, and flushing only on success. This is a robust pattern that prevents partial flushes on errors.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/component/public.ts (1)

140-150: Missing return validator on replaceOrInsert mutation.

Similar to deleteIfExists, this mutation is missing returns: v.null().

Proposed fix
 export const replaceOrInsert = mutation({
   args: {
     currentKey: v.any(),
     newKey: v.any(),
     value: v.any(),
     summand: v.optional(v.number()),
     namespace: v.optional(v.any()),
     newNamespace: v.optional(v.any()),
   },
+  returns: v.null(),
   handler: replaceOrInsertHandler,
 });
🤖 Fix all issues with AI agents
In @src/client/index.ts:
- Around line 147-154: The currentFlushPromise is left set if
ctx.runMutation(this.component.public.batch, { operations }) rejects; change the
pattern in the flush logic to assign a local promise from ctx.runMutation(...)
and attach a .finally() handler that sets this.currentFlushPromise = null so the
state is always cleared regardless of success or failure; keep awaiting the
assigned promise (await localPromise) and reference unique symbols:
currentFlushPromise, ctx.runMutation, this.component.public.batch, and
operations when locating the code to update.

In @src/component/public.ts:
- Around line 120-123: The deleteIfExists mutation is missing a return
validator; update the export const deleteIfExists = mutation({ ... })
declaration to include a returns: v.null() validator alongside args and handler
(i.e., add returns: v.null() to the mutation options for deleteIfExists) so the
mutation explicitly validates its null return value.
🧹 Nitpick comments (3)
src/component/public.ts (2)

274-291: Consider adding a type-safe sentinel or documenting the collision risk.

The sentinel value "__undefined__" for undefined namespaces could theoretically collide with a namespace that serializes to exactly "__undefined__" (e.g., the literal string "__undefined__"). While unlikely in practice, consider using a Symbol or a more unique sentinel pattern.

Optional: Use Symbol for sentinel
+const UNDEFINED_NAMESPACE_KEY = Symbol("undefined_namespace");
+
 // Helper function to get or create tree for a namespace
 const getTreeForNamespace = async (namespace: any) => {
-  // Use a sentinel value for undefined namespace since JSON.stringify(undefined) returns undefined
-  const key =
-    namespace === undefined
-      ? "__undefined__"
-      : JSON.stringify(convexToJson(namespace));
+  const key: string | symbol =
+    namespace === undefined
+      ? UNDEFINED_NAMESPACE_KEY
+      : JSON.stringify(convexToJson(namespace));
   if (!treesMap.has(key)) {

Note: This would require changing treesMap to Map<string | symbol, ...>.


294-321: Consider adding exhaustive type checking for operation routing.

The if/else chain handles all current operation types, but adding an exhaustive check would catch future additions at compile time and runtime.

Optional: Add exhaustive check
       } else if (op.type === "replaceOrInsert") {
         const { type: _, ...args } = op;
         // Handle delete from original namespace
         const deleteTree = await getTreeForNamespace(op.namespace);
         const newTree = await getTreeForNamespace(op.newNamespace);
         await replaceOrInsertHandler(ctx, args, deleteTree, newTree);
+      } else {
+        const _exhaustiveCheck: never = op;
+        throw new Error(`Unknown operation type: ${(op as any).type}`);
       }
     }
example/convex/batchedWrites.ts (1)

311-352: Consider using unique keys to avoid collision on repeated runs.

The hardcoded key offset 1000000 and id pattern perf-test-${i} will cause collisions if this mutation is run multiple times without clearing the aggregate first. Consider using a timestamp or random prefix for truly independent runs.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3efa4bc and 456bc21.

📒 Files selected for processing (3)
  • example/convex/batchedWrites.ts
  • src/client/index.ts
  • src/component/public.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/batchedWrites.ts
🧠 Learnings (28)
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if ctx.runMutation throws an error, the entire mutation is rolled back, including any in-memory state changes and queued operations. Do not implement or rely on manual queue-preservation or compensating logic in response to a failed runMutation, as it would undermine the transactional guarantees. When reviewing TypeScript files under src that use Convex mutations, assume that no explicit preservation of pre-call queues is needed for rollback scenarios; focus on correctness of the mutation call itself and downstream effects, not on attempting to manually undo queued work.

Applied to files:

  • src/client/index.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: Convex runs in a deterministic execution environment without transient failures (no network flakes, etc.). Failures in `ctx.runMutation` only occur due to logical bugs, resource limits (reading too much data), or deterministic errors, so retrying the same operations has near-zero likelihood of succeeding.

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.patch` to shallow merge updates into an existing document; this method throws an error if the document does not exist

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Never use `ctx.db` inside of an action; actions do not have access to the database

Applied to files:

  • src/client/index.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runAction` to call an action from another action; otherwise pull out shared code into a helper async function

Applied to files:

  • src/client/index.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • src/client/index.ts
  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • example/convex/batchedWrites.ts
  • src/component/public.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if `ctx.runMutation` throws, the entire mutation rolls back, including any in-memory state changes made before the call. This transactional guarantee means that operations queued before a failed `ctx.runMutation` call will be automatically restored, so explicit error handling to preserve queues is unnecessary and would break transactional semantics.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the aggregate repository, return validators are only required for Convex functions in the `src/component` directory, not in `example/` or `convex/` directories.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.int64()` instead of deprecated `v.bigint()` for representing signed 64-bit integers

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/schema.{ts,tsx} : Always define schema in `convex/schema.ts` and import schema definition functions from `convex/server`

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex

Applied to files:

  • src/component/public.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex storage stores items as `Blob` objects; convert all items to/from a `Blob` when using Convex storage

Applied to files:

  • src/component/public.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the get-convex/aggregate repository, return validators are required only for Convex functions located in the src/component directory. Do not require validators for files under example/ or convex/, and limit this rule to TypeScript files under src/component (exclude other paths) indicating that only those files should be governed by the validator policy.

Applied to files:

  • src/component/public.ts
🧬 Code graph analysis (2)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (3)
  • DirectAggregate (736-854)
  • TableAggregate (889-1040)
  • count (175-188)
src/component/public.ts (1)
src/component/btree.ts (7)
  • Key (27-27)
  • Value (28-28)
  • Namespace (29-29)
  • deleteHandler (83-113)
  • insertHandler (45-81)
  • getOrCreateTree (949-985)
  • DEFAULT_MAX_NODE_SIZE (25-25)
🔇 Additional comments (12)
src/component/public.ts (2)

1-14: LGTM - Clean import organization.

The new imports for convexToJson, Value, DatabaseWriter, Key, Namespace, and Doc types are correctly added to support the new handler functions and batch mutation.


87-118: LGTM - Well-structured handler extraction.

The replaceHandler correctly separates delete (from original namespace) and insert (to new namespace) operations, with proper tree injection support for batching.

src/client/index.ts (5)

39-74: LGTM - Well-typed buffered operation union.

The BufferedOperation type correctly mirrors the operation types accepted by the server-side batch mutation, ensuring type consistency between client buffering and server processing.


94-96: LGTM - Instance-level buffering state.

The buffering state is correctly scoped to the Aggregate instance, allowing controlled buffering within a mutation context.


156-168: LGTM - Proper read consistency handling.

The flushBeforeRead method correctly ensures buffered writes are flushed before reads, with a helpful error message when called from a query context.


488-503: LGTM - Consistent buffering pattern in write operations.

The _insert method (and other write methods following the same pattern) correctly queues operations when buffering is active and awaits any in-progress flush when not buffering.


175-188: LGTM - Read operations properly flush before reading.

The count method and other read operations correctly call flushBeforeRead to ensure consistency with buffered writes.

example/convex/batchedWrites.ts (5)

1-23: LGTM - Well-documented example setup.

The file header and aggregate setup are clear. The DirectAggregate is correctly instantiated with the batchedWrites component.


94-120: LGTM - Excellent pattern for combining triggers with buffering.

The customMutation wrapper correctly:

  1. Enables buffering before operations
  2. Wraps the database with triggers
  3. Flushes buffered operations only on success

This is the recommended pattern for trigger-based aggregates.


45-77: LGTM - Clear basic example.

This example effectively demonstrates the fundamental buffering pattern: startBuffering → queue operations → finishBuffering → verify results.


128-160: LGTM - Effective trigger + buffering demonstration.

The addMultipleScores mutation clearly shows how table inserts trigger automatic aggregate updates, and how buffering batches all those updates together.


392-444: LGTM - Good namespace grouping example.

The batchedWritesWithNamespaces mutation effectively demonstrates how operations across multiple namespaces are automatically grouped and processed efficiently.

Comment on lines +147 to +154
this.currentFlushPromise = ctx
.runMutation(this.component.public.batch, {
operations,
})
.then(() => (this.currentFlushPromise = null));
// end critical section
await this.currentFlushPromise;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential issue: currentFlushPromise not cleared on mutation failure.

If ctx.runMutation throws, the .then() callback won't execute, leaving currentFlushPromise set. Subsequent calls will wait indefinitely on the rejected promise (which actually resolves/rejects, so it won't hang, but the state is inconsistent). Consider using .finally() to ensure cleanup.

Proposed fix
     this.currentFlushPromise = ctx
       .runMutation(this.component.public.batch, {
         operations,
       })
-      .then(() => (this.currentFlushPromise = null));
+      .finally(() => (this.currentFlushPromise = null));
     // end critical section
     await this.currentFlushPromise;
🤖 Prompt for AI Agents
In @src/client/index.ts around lines 147 - 154, The currentFlushPromise is left
set if ctx.runMutation(this.component.public.batch, { operations }) rejects;
change the pattern in the flush logic to assign a local promise from
ctx.runMutation(...) and attach a .finally() handler that sets
this.currentFlushPromise = null so the state is always cleared regardless of
success or failure; keep awaiting the assigned promise (await localPromise) and
reference unique symbols: currentFlushPromise, ctx.runMutation,
this.component.public.batch, and operations when locating the code to update.

Comment on lines 120 to 123
export const deleteIfExists = mutation({
args: { key: v.any(), namespace: v.optional(v.any()) },
handler: async (ctx, { key, namespace }) => {
try {
await deleteHandler(ctx, { key, namespace });
} catch (e) {
if (e instanceof ConvexError && e.data?.code === "DELETE_MISSING_KEY") {
return;
}
throw e;
}
},
handler: deleteIfExistsHandler,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing return validator on deleteIfExists mutation.

Per Convex best practices, all mutations should include a return validator. This mutation is missing returns: v.null().

Proposed fix
 export const deleteIfExists = mutation({
   args: { key: v.any(), namespace: v.optional(v.any()) },
+  returns: v.null(),
   handler: deleteIfExistsHandler,
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const deleteIfExists = mutation({
args: { key: v.any(), namespace: v.optional(v.any()) },
handler: async (ctx, { key, namespace }) => {
try {
await deleteHandler(ctx, { key, namespace });
} catch (e) {
if (e instanceof ConvexError && e.data?.code === "DELETE_MISSING_KEY") {
return;
}
throw e;
}
},
handler: deleteIfExistsHandler,
});
export const deleteIfExists = mutation({
args: { key: v.any(), namespace: v.optional(v.any()) },
returns: v.null(),
handler: deleteIfExistsHandler,
});
🤖 Prompt for AI Agents
In @src/component/public.ts around lines 120 - 123, The deleteIfExists mutation
is missing a return validator; update the export const deleteIfExists =
mutation({ ... }) declaration to include a returns: v.null() validator alongside
args and handler (i.e., add returns: v.null() to the mutation options for
deleteIfExists) so the mutation explicitly validates its null return value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
example/convex/batchedWrites.ts (2)

187-236: Good performance comparison implementation.

The mutation correctly demonstrates batching vs. non-batching performance. The use of customCtx for database operations (lines 205, 219) and ctx for aggregate operations (lines 197, 213) is correct.

Optional improvement: Consider adding a label to console.time() on Line 198 for better log clarity, e.g., console.time("compare-overall").

📊 Optional: Add label for clarity
-    console.time();
+    console.time("compare-overall");
     if (useBatching) {
-    console.timeEnd();
+    console.timeEnd("compare-overall");

392-444: Good namespace batching example.

The mutation demonstrates how operations across multiple namespaces are automatically grouped and optimized in a single batch call.

Optional improvement: Consider moving the namespacedAggregate instance (lines 412-416) to module level alongside the other aggregates. This avoids creating a new instance on each invocation. However, since this is an example file demonstrating the pattern, the current approach is acceptable.

♻️ Optional: Move to module level

At the top of the file, after the leaderboardAggregate definition:

const namespacedAggregate = new DirectAggregate<{
  Key: number;
  Id: string;
  Namespace: string;
}>(components.batchedWrites);

Then in the handler, remove lines 412-416 and use the module-level instance directly.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 456bc21 and 462c727.

📒 Files selected for processing (1)
  • example/convex/batchedWrites.ts
🧰 Additional context used
📓 Path-based instructions (1)
example/convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (example/.cursor/rules/convex_rules.mdc)

example/convex/**/*.{ts,tsx}: ALWAYS use the new function syntax for Convex functions with query, mutation, action decorators and handler property
Use array validators with v.array() to specify element types in Convex function arguments and schemas
Use v.null() validator when returning or defining fields that can be null values in Convex functions
Use v.int64() instead of deprecated v.bigint() for representing signed 64-bit integers
Use v.record() for defining record types; v.map() and v.set() are not supported in Convex
ALWAYS include argument and return validators for all Convex functions including query, internalQuery, mutation, internalMutation, action, and internalAction; use returns: v.null() if function doesn't return anything
Index fields must be queried in the same order they are defined; create separate indexes if you need different query orders
Use Id<'tableName'> TypeScript helper type imported from './_generated/dataModel' for strict typing of document IDs
Use as const for string literals in discriminated union types in TypeScript
Always define arrays as const array: Array<T> = [...] with explicit type annotation
Always define records as const record: Record<KeyType, ValueType> = {...} with explicit type annotation
Use internalQuery, internalMutation, and internalAction to register private functions that are only callable by other Convex functions
Use query, mutation, and action to register public functions exposed to the API; do NOT use these for sensitive internal functions
Use ctx.runQuery to call a query from a query, mutation, or action
Use ctx.runMutation to call a mutation from a mutation or action
Use ctx.runAction to call an action from another action; otherwise pull out shared code into a helper async function
When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value
Organize files with publi...

Files:

  • example/convex/batchedWrites.ts
🧠 Learnings (20)
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Organize files with public query, mutation, or action functions thoughtfully within the `convex/` directory using file-based routing

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `internalQuery`, `internalMutation`, and `internalAction` to register private functions that are only callable by other Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators and `handler` property

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `query`, `mutation`, and `action` to register public functions exposed to the API; do NOT use these for sensitive internal functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `for await (const row of query)` syntax for async iteration; do not use `.collect()` or `.take(n)` on query results

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runMutation` to call a mutation from a mutation or action

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`; use `returns: v.null()` if function doesn't return anything

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Convex queries do NOT support `.delete()`; instead `.collect()` the results and call `ctx.db.delete(row._id)` on each result

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.record()` for defining record types; `v.map()` and `v.set()` are not supported in Convex

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: In Convex mutations, if `ctx.runMutation` throws, the entire mutation rolls back, including any in-memory state changes made before the call. This transactional guarantee means that operations queued before a failed `ctx.runMutation` call will be automatically restored, so explicit error handling to preserve queues is unnecessary and would break transactional semantics.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-05T03:45:23.438Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:60-72
Timestamp: 2025-12-05T03:45:23.438Z
Learning: In Convex, mutations are run in isolated environments where globals are not shared across mutation invocations. A global variable set in one mutation invocation will not affect subsequent mutation invocations.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T01:39:30.164Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: src/client/index.ts:120-139
Timestamp: 2026-01-10T01:39:30.164Z
Learning: Convex runs in a deterministic execution environment without transient failures (no network flakes, etc.). Failures in `ctx.runMutation` only occur due to logical bugs, resource limits (reading too much data), or deterministic errors, so retrying the same operations has near-zero likelihood of succeeding.

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.runQuery` to call a query from a query, mutation, or action

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `paginationOptsValidator` with `numItems` and `cursor` properties; paginated queries return objects with `page`, `isDone`, and `continueCursor` properties

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `ctx.db.replace` to fully replace an existing document; this method throws an error if the document does not exist

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use `v.null()` validator when returning or defining fields that can be null values in Convex functions

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Add `'use node';` to the top of files containing actions that use Node.js built-in modules

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2025-12-03T18:51:23.434Z
Learnt from: CR
Repo: get-convex/aggregate PR: 0
File: example/.cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-03T18:51:23.434Z
Learning: Applies to example/convex/**/*.{ts,tsx} : Use array validators with `v.array()` to specify element types in Convex function arguments and schemas

Applied to files:

  • example/convex/batchedWrites.ts
📚 Learning: 2026-01-10T03:11:30.815Z
Learnt from: ianmacartney
Repo: get-convex/aggregate PR: 167
File: example/convex/batchedWrites.ts:0-0
Timestamp: 2026-01-10T03:11:30.815Z
Learning: In the aggregate repository, return validators are only required for Convex functions in the `src/component` directory, not in `example/` or `convex/` directories.

Applied to files:

  • example/convex/batchedWrites.ts
🧬 Code graph analysis (1)
example/convex/batchedWrites.ts (2)
src/component/schema.ts (1)
  • aggregate (25-28)
src/client/index.ts (3)
  • DirectAggregate (736-854)
  • TableAggregate (889-1040)
  • count (175-188)
🔇 Additional comments (9)
example/convex/batchedWrites.ts (9)

1-18: Excellent documentation and setup!

The file header clearly explains the batching concept and its benefits, especially for trigger-based workflows. The imports follow Convex conventions correctly.


20-40: LGTM! Clean setup of aggregate instances.

The module-level aggregate instances follow the correct pattern. Using negative score for descending leaderboard order on Line 31 is an elegant solution. The reset mutation correctly uses internalMutation for internal-only access.


42-77: Excellent demonstration of basic batching pattern!

The mutation correctly pairs startBuffering() with finishBuffering() and includes validation to ensure correctness. The pattern of queuing operations during buffering and then flushing is implemented correctly.


79-120: Excellent pattern for combining triggers with batching!

The customMutation wrapper correctly implements the recommended pattern: enabling buffering before the handler runs and flushing on success. The lack of an onFailure handler is correct because Convex's transactional rollback automatically restores the buffering state.


122-160: Great example of trigger-based batching!

The mutation demonstrates how triggers automatically update the aggregate while batching consolidates all updates into a single component call. The validation logic (lines 148-153) ensures the sum values are correctly maintained.


162-185: LGTM! Proper use of ctx.db.patch.

The mutation correctly demonstrates batched replace operations via triggers. Using ctx.db.patch on Line 177 is the appropriate Convex API for updating existing documents.


238-306: Excellent demonstration of mixed operation batching!

This mutation showcases how different operation types (inserts, deletes, updates) can be efficiently batched together. The use of idempotent variants (deleteIfExists on Line 280, replaceOrInsert on Line 288) is appropriate for this pattern.


308-352: Clean performance comparison!

The mutation provides a straightforward batching vs. unbatched comparison. Using high key values (1000000+) on lines 325 and 338 is a good practice to avoid conflicts with other examples in the file.


354-390: Great demonstration of auto-flush behavior!

This mutation effectively shows how read operations automatically flush pending writes before executing. The comment on lines 374-375 clearly explains this behavior, making it educational for users of the library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants